Skip to content

add sandbox PoC#99

Draft
amir-at-bunny wants to merge 3 commits into
mainfrom
poc/sandbox
Draft

add sandbox PoC#99
amir-at-bunny wants to merge 3 commits into
mainfrom
poc/sandbox

Conversation

@amir-at-bunny

Copy link
Copy Markdown
Collaborator

No description provided.

@changeset-bot

changeset-bot Bot commented Jun 18, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 1ec4b00

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@jamie-at-bunny

Copy link
Copy Markdown
Member

Hey @amir-at-bunny, this looks awesome! Can you run bun lint and fix any issues reported?

@amir-at-bunny

Copy link
Copy Markdown
Collaborator Author

@jamie-at-bunny prompt added to the sandbox name as discussed.

@jamie-at-bunny

Copy link
Copy Markdown
Member

@amir-at-bunny is this PR "Ready for review"? Let's get it in 🚀

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@jamie-at-bunny

Copy link
Copy Markdown
Member

@greptile-apps review

@greptile-apps

greptile-apps Bot commented Jun 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces a bunny sandbox command group that provisions on-demand Ubuntu containers (via Magic Containers) pre-loaded with Node.js, Bun, and Claude Code, exposes SSH access using a random token as the root password, and manages public CDN endpoints per sandbox. Config, schema, a sandbox Docker image, and a CI workflow to publish it are all included.

  • sandbox create/delete/list/exec/ssh — full lifecycle commands; sandbox metadata (app ID, SSH host, agent token) is persisted in ~/.config/bunnynet.json.
  • sandbox url add/list/delete — manages per-sandbox CDN endpoints for exposing container ports publicly.
  • sandbox/Dockerfile + entrypoint.sh — Ubuntu 24.04 image with openssh-server on port 8023; root password is set at startup from $AGENT_TOKEN.

Confidence Score: 3/5

Several concrete defects need fixing before this reaches users: the SSL/https mismatch will produce broken URLs, the unquoted cwd will silently misbehave on any path with a space, and the missing JSON output branch will break any scripted consumer.

The create command never checks output === 'json' so piping its output into a script always yields human-readable text. The url add/list commands create a plain-HTTP CDN endpoint but hand users an https:// URL that won't work. The exec remote command builds a raw shell string from the user-supplied --cwd without quoting, so any path containing a space fails silently. The imagePullPolicy choice means deployed sandboxes will run stale images indefinitely once pulled. These are present, concrete misbehaviours on the changed code paths.

packages/cli/src/commands/sandbox/create.ts, packages/cli/src/commands/sandbox/exec.ts, packages/cli/src/commands/sandbox/url/add.ts, packages/cli/src/commands/sandbox/url/list.ts

Security Review

  • Host-key verification disabled on every SSH connection (ssh-exec.ts): StrictHostKeyChecking=no + UserKnownHostsFile=/dev/null means the SSH client accepts any presented host key, making every exec and ssh call silently vulnerable to a machine-in-the-middle substitution over the public anycast endpoint.
  • agent_token stored in plaintext config: The token doubles as the SSH root password and is written to ~/.config/bunnynet.json with no additional protection beyond the file permissions already applied to that config file.
  • sshpass -p exposes the token in the process argument list: On multi-user systems, the password can be read from /proc/<pid>/cmdline by other local users for the duration of the SSH handshake.

Important Files Changed

Filename Overview
packages/cli/src/commands/sandbox/create.ts Creates sandboxes via Magic Containers API; no JSON output path, imagePullPolicy ifNotPresent with latest tag silently pins stale images, and inline App type + as any casts bypass generated schema types.
packages/cli/src/commands/sandbox/exec.ts Runs SSH commands in sandboxes; unquoted cwd in the remote command string breaks paths with spaces and allows shell injection via the --cwd flag.
packages/cli/src/commands/sandbox/url/add.ts Exposes container ports as CDN endpoints; creates the endpoint with isSslEnabled:false but displays the resulting URL as https://, producing broken links.
packages/cli/src/commands/sandbox/ssh-exec.ts Shared SSH argument builder; disables host-key verification on every connection exposing connections to MITM over the public anycast endpoint.
packages/cli/src/commands/sandbox/url/list.ts Lists CDN endpoints for a sandbox; filters built-in endpoints correctly but mirrors the https:// prefix bug from url/add.ts.
packages/cli/src/commands/sandbox/delete.ts Deletes sandbox and its MC app with optional --force; confirmation flow and local config cleanup look correct.
packages/cli/src/commands/sandbox/list.ts Lists sandboxes from local config file; table and JSON output both handled correctly.
packages/cli/src/commands/sandbox/ssh.ts Opens an interactive SSH session into a sandbox; straightforward implementation, relies on ssh-exec.ts for argument construction.
packages/cli/src/config/schema.ts Adds SandboxRecord Zod schema and extends ConfigFileSchema; schema shape is correct and backward compatible.
packages/cli/src/config/index.ts Adds getSandbox/setSandbox/deleteSandbox helpers; read-modify-write pattern matches existing setProfile convention.
sandbox/Dockerfile Ubuntu 24.04 image with Node.js LTS, Bun, Claude Code, and openssh-server on port 8023; root password auth and PermitRootLogin are intentional PoC choices.
sandbox/entrypoint.sh Sets root password to AGENT_TOKEN at container start and launches sshd; simple and correct for the intended design.
.github/workflows/sandbox-agent.yml CI workflow to build and push sandbox Docker image to GHCR on pushes to main affecting sandbox/**; only pushes latest tag with no digest pinning.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant CLI
    participant MC_API as Magic Containers API
    participant Sandbox as Sandbox Container

    User->>CLI: bunny sandbox create [name] [--region]
    CLI->>MC_API: POST /apps (image, volumes, SSH endpoint)
    MC_API-->>CLI: "app { id }"
    loop Poll until anycast SSH endpoint assigned
        CLI->>MC_API: "GET /apps/{appId}"
        MC_API-->>CLI: "app { status, endpoints[] }"
    end
    loop Probe TCP until SSH port accepts connections
        CLI-->>Sandbox: TCP connect :8023
    end
    CLI->>CLI: "setSandbox(name, { app_id, agent_token, ssh_host })"
    CLI-->>User: Sandbox ready (app_id, ssh_host)

    User->>CLI: bunny sandbox exec [name] [cmd] [--cwd]
    CLI->>CLI: getSandbox(name) → record
    CLI->>Sandbox: "sshpass -p token ssh root@host cmd"
    Sandbox-->>User: stdout/stderr (exit code propagated)

    User->>CLI: bunny sandbox url add [name] [port] [--label]
    CLI->>MC_API: "POST /apps/{appId}/containers/{id}/endpoints"
    loop Poll until publicHost assigned
        CLI->>MC_API: "GET /apps/{appId}/endpoints"
        MC_API-->>CLI: "endpoint { publicHost }"
    end
    CLI-->>User: https://publicHost

    User->>CLI: bunny sandbox delete [name] [--force]
    CLI->>MC_API: "DELETE /apps/{appId}"
    CLI->>CLI: deleteSandbox(name)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant CLI
    participant MC_API as Magic Containers API
    participant Sandbox as Sandbox Container

    User->>CLI: bunny sandbox create [name] [--region]
    CLI->>MC_API: POST /apps (image, volumes, SSH endpoint)
    MC_API-->>CLI: "app { id }"
    loop Poll until anycast SSH endpoint assigned
        CLI->>MC_API: "GET /apps/{appId}"
        MC_API-->>CLI: "app { status, endpoints[] }"
    end
    loop Probe TCP until SSH port accepts connections
        CLI-->>Sandbox: TCP connect :8023
    end
    CLI->>CLI: "setSandbox(name, { app_id, agent_token, ssh_host })"
    CLI-->>User: Sandbox ready (app_id, ssh_host)

    User->>CLI: bunny sandbox exec [name] [cmd] [--cwd]
    CLI->>CLI: getSandbox(name) → record
    CLI->>Sandbox: "sshpass -p token ssh root@host cmd"
    Sandbox-->>User: stdout/stderr (exit code propagated)

    User->>CLI: bunny sandbox url add [name] [port] [--label]
    CLI->>MC_API: "POST /apps/{appId}/containers/{id}/endpoints"
    loop Poll until publicHost assigned
        CLI->>MC_API: "GET /apps/{appId}/endpoints"
        MC_API-->>CLI: "endpoint { publicHost }"
    end
    CLI-->>User: https://publicHost

    User->>CLI: bunny sandbox delete [name] [--force]
    CLI->>MC_API: "DELETE /apps/{appId}"
    CLI->>CLI: deleteSandbox(name)
Loading

Comments Outside Diff (4)

  1. packages/cli/src/commands/sandbox/url/add.ts, line 1004-1006 (link)

    P1 SSL disabled but URL shown as https://

    The endpoint is created with isSslEnabled: false, but the URL printed to the user (and also in url/list.ts) is always prefixed with https://. If the CDN endpoint genuinely has no SSL, every link handed to the user will fail in a browser with a TLS error. Either enable SSL at creation time or prefix the URL with http:// to match the actual endpoint configuration.

    Fix in Claude Code

  2. packages/cli/src/commands/sandbox/create.ts, line 574-588 (link)

    P1 sandbox create has no JSON output path

    The AGENTS.md convention requires every command that returns data to support --output json. The handler receives output but never checks for output === "json" — callers using --output json receive human-readable logger.log lines, breaking any scripted or agent usage. A JSON branch should return a structured object (e.g. { name, app_id, ssh_host }) before the plain-text log block.

    Fix in Claude Code

  3. packages/cli/src/commands/sandbox/ssh-exec.ts, line 24-40 (link)

    P2 security Host key verification fully disabled

    StrictHostKeyChecking=no combined with UserKnownHostsFile=/dev/null means the SSH client accepts any host key without verification on every connection, making the connection silently vulnerable to a machine-in-the-middle attack over the public anycast endpoint. Consider -o StrictHostKeyChecking=accept-new so the key is pinned on first connect and validated on subsequent ones.

    Fix in Claude Code

  4. packages/cli/src/commands/sandbox/create.ts, line 359-375 (link)

    P2 Inline App type instead of generated schema types

    AGENTS.md and CLAUDE.md both require using Pick<components["schemas"]["TypeName"], ...> from the generated Magic Containers spec instead of hand-rolled record types. The inline App type here, and the (client as any).POST cast for the create call, hide type errors. Consider defining a CustomPaths entry for the undocumented POST /apps endpoint following the existing pattern in core-client.ts.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

    Fix in Claude Code

Fix All in Claude Code

Reviews (1): Last reviewed commit: "add prompt to sandbox names" | Re-trigger Greptile

);
}

const remoteCmd = `cd ${cwd} && ${command.join(" ")}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Unquoted cwd breaks paths with spaces and is susceptible to shell injection

cwd is user-supplied via --cwd. Any path containing spaces (e.g. --cwd "/my project") will silently fail the cd, and a value like --cwd '/tmp; rm -rf /workplace' will run extra shell commands in the sandbox. Quoting the path with JSON.stringify (which produces a double-quoted, properly escaped shell string) prevents both issues.

Suggested change
const remoteCmd = `cd ${cwd} && ${command.join(" ")}`;
const remoteCmd = `cd ${JSON.stringify(cwd)} && ${command.join(" ")}`;

Fix in Claude Code

imageNamespace: IMAGE_NAMESPACE,
imageName: IMAGE_NAME,
imageTag: IMAGE_TAG,
imagePullPolicy: "ifNotPresent",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 ifNotPresent with a latest tag means sandbox images are never updated

When the image policy is ifNotPresent and the tag is latest, the runtime pulls the image once and then never re-pulls it, even if a new latest is pushed (e.g. a security patch to Claude Code or Bun). Using always ensures each sandbox creation picks up the most recent build.

Suggested change
imagePullPolicy: "ifNotPresent",
imagePullPolicy: "always",

Fix in Claude Code

@amir-at-bunny

Copy link
Copy Markdown
Collaborator Author

@amir-at-bunny is this PR "Ready for review"? Let's get it in 🚀

yep. thanks i'll take a look at the review in a bit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants